/*
 * User: user
 * Date: 08/04/2008
 * Time: 23:16:03
 */
package org.conann.container;

import static com.google.common.base.Preconditions.checkNotNull;
import net.jcip.annotations.Immutable;
import org.conann.exceptions.WebBeansInstantiationException;
import org.conann.exceptions.WebBeansMetadataException;
import static org.conann.util.AnnotationUtil.getRelevantAnnotations;
import org.conann.util.DevelopmentUtil;

import javax.annotation.PostConstruct;
import javax.webbeans.Initializer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@Immutable
public class InstanceCreator<T> implements Creator<T> {

    private final Class<T> type;
    /** The component constructor */
    private final Constructor<T> constructor;
    private final Method postCreateMethod;

    public InstanceCreator(Class<T> type) {
        checkNotNull(type);
        this.type = type;
        constructor = resolveConstructor();
        postCreateMethod = resolvePostCreateMethod();
    }

    public T create(Object... instantiationParams) {
        T instance = instantiate(instantiationParams);
        invokePostCreation(instance);
        return instance;
    }

    private void invokePostCreation(T instance) {
        throw DevelopmentUtil.unsupportedOperation();
    }

    T instantiate(Object... constructorParams) {
        try {
            return constructor.newInstance(constructorParams);
        } catch (InstantiationException e) {
            throw createInstantiationException(e, constructorParams);
        } catch (IllegalAccessException e) {
            throw createInstantiationException(e, constructorParams);
        } catch (InvocationTargetException e) {
            throw createInstantiationException(e, constructorParams);
        }
    }

    private WebBeansInstantiationException createInstantiationException(Exception e, Object... constructorParams) {
        return new WebBeansInstantiationException(e, "Could not instantiate type %s\n with the following parameters:\n%s", type, constructorParams);
    }

    private Method resolvePostCreateMethod() {
        Method matchingMethod = null;
        for (Method method : type.getDeclaredMethods()) {
            if (method.isAnnotationPresent(PostConstruct.class)) {
                if (matchingMethod != null) {
                    // there can be only 1 :)
                    throw new WebBeansMetadataException("There are more than 1 method in [%s] annotated with @%s annotation.\n", type, PostConstruct.class);
                }
                method.setAccessible(true);
                matchingMethod = method;
            }
        }
        return matchingMethod;
    }

    private Constructor<T> resolveConstructor() {
        Constructor<?>[] declaredConstructors = type.getDeclaredConstructors();

        // if type declares exactly 1 constructor, that's our guy:
        if (declaredConstructors.length == 1) {
            return (Constructor<T>) declaredConstructors[0];
        }

        // type declares more than 1 contructor.
        // exactly one of these constructors must be annotated @Initializer
        // or have a parameter annotated with a binding type.
        // This constructor is the component constructor.
        Constructor<?> matchingConstructor = null;
        for (Constructor<?> constructor : declaredConstructors) {
            if (constructor.isAnnotationPresent(Initializer.class) || areParametersAnnotatedWithBindingTypes(constructor)) {
                if (matchingConstructor != null) {
                    throw new WebBeansMetadataException("Could not resolve a constructor for type [%s].\nMore than 1 matching constructor were found.", type.getName());
                }
                matchingConstructor = constructor;
            }
        }
        if (matchingConstructor == null) {
            throw new WebBeansMetadataException("Could not resolve a constructor for type [%s]." + getContructorResolutionErrorMessage(), type.getName());
        }
        return (Constructor<T>) matchingConstructor;
    }

    private boolean areParametersAnnotatedWithBindingTypes(Constructor<?> constructor) {
        for (Annotation[] annotations : constructor.getParameterAnnotations()) {
            if (!getRelevantAnnotations(annotations).isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private String getContructorResolutionErrorMessage() {
        return String.format("Your options for a valid component constructor are:\n" + "1. Use the default empty constructor.\n" + "2. Declare a single constructor.\n" + "3. Annotate your designated contructor with an @%s annotation.\n" + "4. Use a binding annotation for at least one of your designated constructor's parameters.", Initializer.class);
    }
}